<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>PLT(HPGL)编辑器 - 全屏画布 + 悬浮按钮</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root{
  --panel-bg:#ffffff;
  --accent:#26a69a;
  --accent-dark:#18857e;
  --muted:#1b2228;
  --overlay-bg: rgba(255,255,255,0.9);
  --shadow: 0 4px 14px rgba(0,0,0,0.12);
}
*{box-sizing:border-box}
body{ font-family:"Segoe UI",Arial,sans-serif; margin:0; background:#f2f5f7; color:#111;}
header{padding:12px 18px; background:linear-gradient(90deg,#fff,#f7fbfa); border-bottom:1px solid #e6ebea;}
h1{margin:0;font-size:18px;display:inline-block}
.top-actions{display:inline-block; margin-left:18px; vertical-align:middle;}
.container{display:flex; gap:14px; padding:12px; align-items:flex-start;}

/* 左侧主画布区域，占据大部分空间，支持全屏 */
.canvas-wrap{
  position:relative;
  flex:1 1 auto;
  background:#fff;
  border:1px solid #cfcfcf;
  border-radius:8px;
  overflow:hidden;
  min-height:420px;
  height: calc(100vh - 190px); /* 初始非全屏高度（留出头部和帮助栏） */
  box-shadow:var(--shadow);
}

/* canvas 充满容器 */
#canvas{ display:block; width:100%; height:100%; background:#f5fafb; cursor:grab; }

/* 悬浮工具条（左上） */
.floating-bar{
  position:absolute;
  top:12px;
  left:12px;
  display:flex;
  gap:8px;
  padding:8px;
  background:var(--overlay-bg);
  border-radius:8px;
  box-shadow:var(--shadow);
  z-index:50;
  align-items:center;
}
.floating-bar button{
  background:var(--accent);
  color:#fff;border:0;padding:6px 10px;border-radius:6px;
  cursor:pointer;font-size:15px;transition:background .14s;
}
.floating-bar button:active{background:var(--accent-dark);}

/* disabled 样式：置灰且禁止交互 */
.floating-bar button:disabled{
  background:#ccc;
  color:#666;
  cursor:not-allowed;
  opacity:0.75;
  pointer-events:none;
}

.floating-bar .icon{font-weight:bold}

/* 悬浮右侧控件（右上） */
.floating-right{
  position:absolute;
  right:12px;
  top:12px;
  padding:10px;
  background:var(--overlay-bg);
  border-radius:8px;
  box-shadow:var(--shadow);
  z-index:50;
  min-width:240px;
}
.floating-right label{display:flex;align-items:center;gap:10px; font-size:14px;}
.floating-right input[type=range]{flex:1;}
.floating-right .row{margin-top:8px; display:flex; gap:8px; justify-content:flex-end;}
.floating-right .button{background:var(--accent); color:#fff; border:none; padding:7px 10px; border-radius:6px; cursor:pointer}

/* 底部信息区 */
.side-panel{
  width:320px;
  min-width:260px;
  background:var(--panel-bg);
  border-radius:8px;
  padding:12px;
  box-shadow:var(--shadow);
  max-height: calc(100vh - 190px);
  overflow:auto;
}
#help ul{margin:8px 0 0 1em;padding:0;font-size:14px}
#info,#status{font-size:13px;margin-top:6px;color:#333}

/* 弹窗选择 */
.disambig-popup{
    position: absolute; background: #fafcfc;
    border:1.3px solid var(--accent);border-radius:6px;box-shadow:0 6px 18px rgba(0,0,0,0.18);
    padding:8px 10px;z-index:120;font-size:14px;
}
.disambig-popup button{
    margin:6px 6px 0 0; padding:6px 8px;
    background: #e8fffb;border:1px solid var(--accent); border-radius:5px; cursor: pointer;}
.disambig-popup button:hover{background:var(--accent); color:#fff;}

/* 底部帮助区 */
.help-bottom{padding:10px 14px; margin:12px; background:var(--panel-bg); border-radius:8px; box-shadow:var(--shadow);}

/* 小屏时调整 */
@media(max-width:900px){
  .container{flex-direction:column;padding:8px;}
  .side-panel{width:100%}
  .canvas-wrap{height:60vh}
}
</style>
</head>
<body>
<header>
  <h1>PLT(HPGL) 编辑器</h1>
  <div class="top-actions">
    <input type="file" id="fileInput" accept=".plt">
    <span id="info" style="margin-left:8px;"></span>
  </div>
</header>

<div class="container">
  <div class="canvas-wrap" id="canvas_box">
    <canvas id="canvas"></canvas>

    <!-- 悬浮左上工具条 -->
    <div class="floating-bar" id="floatingBar">
      <button id="btnZoomIn" title="放大">＋</button>
      <button id="btnZoomOut" title="缩小">－</button>
      <button id="btnReset" title="复位全图">全图</button>
      <button id="btnUndo" title="撤销 Ctrl+Z">撤销</button>
      <button id="btnRedo" title="重做 Ctrl+Y">重做</button>
      <button id="btnFull" title="切换全屏">⛶</button>
    </div>

    <!-- 悬浮右上控件 -->
    <div class="floating-right" id="floatingRight">
      <label>
        按长度过滤
        <input type="range" id="lengthFilter" min="0" max="10000" step="10" value="0">
      </label>
      <div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px;">
        <div style="font-size:13px;color:#444">值：<span id="lengthValue">0</span></div>
        <div>
          <button class="button" id="deleteSelected">删除</button>
          <button class="button" id="downloadPLT">下载</button>
        </div>
      </div>
      <div id="status" style="margin-top:8px;font-size:13px;color:#444"></div>
    </div>

    <div id="disambig-popup" class="disambig-popup" style="display:none;"></div>
  </div>

  <div class="side-panel">
    <b>用法 & 功能说明</b>
    <div id="help">
      <ul>
        <li>上传 .plt 后自动居中并铺满画布。</li>
        <li>左键拖动画布（顺滑）。左键点击选线（多选）。</li>
        <li>右键单击：若附近多线会弹窗供精确选择；单条直接切换选中。</li>
        <li>支持删除、撤销/重做（按钮或 Ctrl+Z / Ctrl+Y）。</li>
        <li>滑动“按长度过滤”实时生效，但仅在松手时记录一次历史，避免产生大量中间步骤。</li>
        <li>支持下载修改后的 PLT。</li>
      </ul>
    </div>
    <div style="margin-top:10px;font-size:13px;color:#666">
      <div>文件: <span id="fileLabel">未加载</span></div>
      <div style="margin-top:6px" id="metaInfo"></div>
    </div>
  </div>
</div>

<div class="help-bottom" style="margin:12px 14px 20px 14px;">
  更多说明：ESC 清除选中；Delete/Backspace 删除选中线；画布支持滚轮缩放。
</div>

<script>
/* 主要逻辑与之前保持一致，做了画布响应式、全屏与浮动工具栏 UI 改进 */
let lines=[], original_lines=[], lengths=[], deleted_idxs=new Set(), selected_line_idxs=new Set();
let minX=0, minY=0, maxX=1, maxY=1, dataW=1, dataH=1, kx=1, ky=1;
let offsetX=0, offsetY=0, zoom=1, viewX=0, viewY=0;
const canvas=document.getElementById('canvas'), ctx=canvas.getContext('2d');
const canvasBox = document.getElementById('canvas_box');
let disambigPopup = document.getElementById('disambig-popup');
let curFileName = '';

let undoStack=[], redoStack=[], maxHistory=50;

/* 拖拽行为：左键顺滑拖动；右键用于选择 */
let leftMouseDown = false;
let leftDownX = 0, leftDownY = 0;
let leftStartViewX = 0, leftStartViewY = 0;
let leftDragStarted = false;
let dragThreshold = 4;

/* 滑块提交历史控制 */
let committedFilterValue = 0;
let committedSnapshot = null;
let isSliding = false;
let pendingFilterSnapshot = null;

/* 初始化画布大小（根据容器大小），并在 resize 时调用 */
function resizeCanvasToFit(){
    // 使用容器（canvasBox）的尺寸，确保全屏时 canvas 也跟随变大
    const dpr = Math.max(1, window.devicePixelRatio || 1);
    const rect = canvasBox.getBoundingClientRect();
    // 确保 canvas CSS 占满容器
    canvas.style.width = rect.width + 'px';
    canvas.style.height = rect.height + 'px';
    // 设置画布像素大小
    canvas.width = Math.round(rect.width * dpr);
    canvas.height = Math.round(rect.height * dpr);
    // 将坐标映射恢复到 CSS 像素空间
    ctx.setTransform(dpr,0,0,dpr,0,0);
    redrawAll();
}

/* 操作历史 */
function pushHistory(type, dat){
    let hist={
        type,
        dat: JSON.parse(JSON.stringify(dat||{})),
        lines:JSON.parse(JSON.stringify(lines)),
        deleted: Array.from(deleted_idxs),
        selected: Array.from(selected_line_idxs)
    };
    undoStack.push(hist);
    if(undoStack.length>maxHistory)undoStack.shift();
    redoStack.length=0;
    refreshUndoRedoBtn();
}
function restoreHistory(hist){
    lines = JSON.parse(JSON.stringify(hist.lines));
    lengths=lines.map(line=>{
        let len=0;
        for(let i=1;i<line.length;++i){
            let dx=line[i][0]-line[i-1][0], dy=line[i][1]-line[i-1][1];
            len+=Math.hypot(dx,dy);
        }
        return len;
    });
    deleted_idxs=new Set(hist.deleted);
    selected_line_idxs=new Set(hist.selected);
    redrawAll();
}
function refreshUndoRedoBtn(){
    document.getElementById('btnUndo').disabled = undoStack.length===0;
    document.getElementById('btnRedo').disabled = redoStack.length===0;
}

/* 解析 PLT */
function parsePLT(content){
    let cmdRE = /(PU|PD)\s*([^;]*);/ig, match, parsed=[], cur=[];
    while((match=cmdRE.exec(content))!==null){
        let cmd=match[1].toUpperCase(), coords=match[2].split(/[, ]+/).filter(x=>x!=='').map(Number);
        let points=[];
        for(let i=0;i<coords.length-1;i+=2)points.push([coords[i],coords[i+1]]);
        if(cmd==='PU'){if(cur.length)parsed.push(cur);cur=[];cur.push(...points);}
        else if(cmd==='PD'){cur.push(...points);}
    }
    if(cur.length)parsed.push(cur);
    return parsed;
}
function computeLengths(){
    lengths=lines.map(line=>{
        let len=0;
        for(let i=1;i<line.length;++i){
            let dx=line[i][0]-line[i-1][0], dy=line[i][1]-line[i-1][1];
            len+=Math.hypot(dx,dy);
        }
        return len;
    });
}
function calcBoundingBox(){
    let pts=[];
    lines.forEach((l,i)=>{if(!deleted_idxs.has(i))pts.push(...l);});
    if(pts.length===0){minX=0;minY=0;maxX=1;maxY=1;}
    else{
        minX=Math.min(...pts.map(p=>p[0]));
        minY=Math.min(...pts.map(p=>p[1]));
        maxX=Math.max(...pts.map(p=>p[0]));
        maxY=Math.max(...pts.map(p=>p[1]));
    }
    dataW=maxX-minX||1; dataH=maxY-minY||1;
}
function autoscaleView(){
    calcBoundingBox();
    let padding=55;
    let canvasW=canvas.clientWidth,canvasH=canvas.clientHeight;
    let W=canvasW-2*padding, H=canvasH-2*padding;
    let scaleW = W/dataW, scaleH = H/dataH;
    let k = Math.min(scaleW, scaleH);
    zoom = 1;
    kx = ky = k;
    let contentDrawWidth = dataW*k, contentDrawHeight = dataH*k;
    let extraX = (canvasW - contentDrawWidth)/2, extraY = (canvasH - contentDrawHeight)/2;
    offsetX = extraX - minX*k;
    offsetY = extraY - minY*k;
    viewX = viewY = 0;
}
function toScreen([x,y]){
    return [
        (x*kx+offsetX)*zoom+viewX,
        (y*ky+offsetY)*zoom+viewY
    ];
}
function draw(){
    // clear using CSS pixel size
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.save();
    lines.forEach((line, idx) => {
        if(deleted_idxs.has(idx)) return;
        ctx.beginPath();
        line.forEach(([x, y], i) => {
            let [sx, sy]=toScreen([x,y]);
            if(i==0)ctx.moveTo(sx,sy);else ctx.lineTo(sx,sy);
        });
        ctx.lineWidth = selected_line_idxs.has(idx)?3.5:1.7;
        ctx.strokeStyle = selected_line_idxs.has(idx)? "#f22":"#1b2228";
        ctx.globalAlpha = selected_line_idxs.has(idx)?0.8:1;
        ctx.stroke();
        ctx.globalAlpha=1;
        let [sx,sy]=toScreen(line[0]);
        ctx.save();
        ctx.font = selected_line_idxs.has(idx) ? "bold 15px Arial" : "14px Arial";
        ctx.fillStyle = selected_line_idxs.has(idx)? "#d22":"#2371e6";
        ctx.fillText(idx, sx+4, sy-6);
        ctx.restore();
    });
    ctx.restore();
}
function updateInfo(){
    document.getElementById('info').textContent =
        (curFileName?("文件："+curFileName+" | "):"")+
        "线条: "+lines.length+"，已选: "+selected_line_idxs.size+
        (deleted_idxs.size>0? "，已删: "+deleted_idxs.size:"");
    document.getElementById('fileLabel').textContent = curFileName || "未加载";
    document.getElementById('metaInfo').textContent = `坐标 X[${minX},${maxX}] Y[${minY},${maxY}]`;
}
function redrawAll(){ draw(); updateInfo(); }

/* 精准找线 */
function findLinesByPoint(x, y, radius=9, maxRetAll=5) {
    let found=[];
    for(let idx=lines.length-1;idx>=0;--idx) {
        if(deleted_idxs.has(idx)) continue;
        let line=lines[idx];
        for(let i=1;i<line.length;i++){
            let [x0,y0]=toScreen(line[i-1]), [x1,y1]=toScreen(line[i]);
            let d=pointToSegmentDistance(x, y, x0, y0, x1, y1);
            if(d<radius){found.push({idx,d});break;}
        }
        if(found.length>=maxRetAll)break;
    }
    return found.sort((a,b)=>a.d-b.d);
}
function pointToSegmentDistance(px,py,x0,y0,x1,y1){
    let A=px-x0,B=py-y0,C=x1-x0,D=y1-y0;
    let dot=A*C+B*D,len_sq=C*C+D*D;
    let t = len_sq ? Math.max(0, Math.min(1, dot / len_sq)) : 0;
    let dx=x0+t*C-px,dy=y0+t*D-py;
    return Math.hypot(dx,dy);
}
function hidePopup(){ disambigPopup.style.display="none"; }

/* 鼠标/交互：左键顺滑拖拽，点击选线；右键弹窗选择 */
canvas.addEventListener('mousedown', function(e){
    if(lines.length===0) return;
    if(e.button===0){
        let rect = canvas.getBoundingClientRect();
        leftMouseDown = true;
        leftDownX = e.clientX - rect.left;
        leftDownY = e.clientY - rect.top;
        leftStartViewX = viewX; leftStartViewY = viewY;
        leftDragStarted = false;
        canvas.style.cursor = "grabbing";
        hidePopup();
    }
});
canvas.addEventListener('mousemove', function(e){
    if(leftMouseDown){
        let rect = canvas.getBoundingClientRect();
        let mx = e.clientX - rect.left, my = e.clientY - rect.top;
        let dx = mx - leftDownX, dy = my - leftDownY;
        if(!leftDragStarted && Math.hypot(dx,dy) > dragThreshold) leftDragStarted = true;
        if(leftDragStarted){
            viewX = leftStartViewX + dx;
            viewY = leftStartViewY + dy;
            redrawAll();
        }
    }
});
canvas.addEventListener('mouseup', function(e){
    if(lines.length===0) return;
    let rect = canvas.getBoundingClientRect();
    let mx = e.clientX - rect.left, my = e.clientY - rect.top;
    if(e.button===0 && leftMouseDown){
        if(!leftDragStarted){
            let candidates = findLinesByPoint(mx,my,11,1);
            if(candidates.length>0){
                pushHistory('select',{});
                let idx=candidates[0].idx;
                if(selected_line_idxs.has(idx)) selected_line_idxs.delete(idx);
                else selected_line_idxs.add(idx);
                redrawAll();
            }
        }else{
            redrawAll();
        }
        leftMouseDown = false; leftDragStarted = false;
        canvas.style.cursor = "grab";
    }
});
window.addEventListener('mouseup', function(){ if(leftMouseDown){ leftMouseDown=false; leftDragStarted=false; canvas.style.cursor="grab"; }});

/* 右键菜单 -> 用作多线选择 */
canvas.addEventListener('contextmenu', function(e){
    if(lines.length===0) return;
    e.preventDefault();
    hidePopup();
    let rect = canvas.getBoundingClientRect();
    let mx = e.clientX - rect.left, my = e.clientY - rect.top;
    let candidates = findLinesByPoint(mx, my, 15, 7);
    if(candidates.length===0) return;
    if(candidates.length===1){
        pushHistory('select',{});
        let idx=candidates[0].idx;
        if(selected_line_idxs.has(idx)) selected_line_idxs.delete(idx);
        else selected_line_idxs.add(idx);
        redrawAll();
    }else{
        showDisambigPopup(candidates, e.clientX, e.clientY);
    }
});

/* 滚轮缩放 */
canvas.addEventListener('wheel', e=>{
    if(lines.length===0) return;
    let rect=canvas.getBoundingClientRect();
    let mx=e.clientX-rect.left, my=e.clientY-rect.top;
    let old_zoom=zoom, scale=(e.deltaY<0)?1.12:0.89;
    zoom=Math.max(zoom*scale,0.03);
    viewX=mx-(mx-viewX)*(zoom/old_zoom);
    viewY=my-(my-viewY)*(zoom/old_zoom);
    redrawAll();
    e.preventDefault();
},{passive:false});

/* 弹窗选线 */
function showDisambigPopup(candidates, pageX, pageY){
    let html="<b>附近有"+candidates.length+"根线，请点击：</b><br/>";
    for(let i=0;i<candidates.length;i++){
        let idx=candidates[i].idx;
        html+=`<button onclick="window.pickLine(${idx})">#${idx} 长度${lengths[idx].toFixed(1)}</button> `;
    }
    disambigPopup.innerHTML=html;
    // 放置相对页面的位置，保证不超出视口
    disambigPopup.style.left = Math.min(window.innerWidth - 260, pageX + 8) + "px";
    disambigPopup.style.top = Math.min(window.innerHeight - 140, pageY - 20) + "px";
    disambigPopup.style.display="block";
}
window.pickLine = function(idx){
    pushHistory('select',{});
    if(selected_line_idxs.has(idx)) selected_line_idxs.delete(idx);
    else selected_line_idxs.add(idx);
    redrawAll(); hidePopup();
}
document.addEventListener('click', function(e){
    if(disambigPopup.style.display==='block' && !disambigPopup.contains(e.target)) hidePopup();
});

/* 文件加载 */
document.getElementById('fileInput').addEventListener('change', function(){
    if(!this.files.length) return;
    let reader = new FileReader();
    reader.onload = function(evt){
        curFileName = document.getElementById('fileInput').files[0].name;
        original_lines = parsePLT(evt.target.result);
        lines = original_lines.map(arr => arr.slice());
        deleted_idxs.clear(); selected_line_idxs.clear();
        computeLengths();
        let maxL = Math.ceil(Math.max(...lengths)) || 1;
        let f = document.getElementById('lengthFilter');
        f.max = maxL + 1000; f.min = 0; f.value = 0;
        document.getElementById('lengthValue').textContent = '0';
        autoscaleView();
        undoStack.length=0; redoStack.length=0; refreshUndoRedoBtn();
        committedFilterValue = +f.value;
        committedSnapshot = { lines: JSON.parse(JSON.stringify(lines)), deleted:Array.from(deleted_idxs), selected:Array.from(selected_line_idxs) };
        pendingFilterSnapshot = null;
        resizeCanvasToFit();
        redrawAll();
        document.getElementById('status').textContent = '';
    };
    reader.readAsText(this.files[0]);
});

/* 过滤滑块：滑动实时生效，松手（pointerup/change）时只记录一次历史 */
const lengthFilter = document.getElementById('lengthFilter');
const lengthValueLabel = document.getElementById('lengthValue');

function applyFilterLive(lv){
    let cntDelete = 0;
    deleted_idxs.clear();
    lines.forEach((_, idx) => { if (lengths[idx] < lv) { deleted_idxs.add(idx); cntDelete++; } });
    selected_line_idxs.clear();
    redrawAll();
    document.getElementById('status').textContent = `已按长度过滤，已隐藏 ${cntDelete} 条短线。`;
}

lengthFilter.addEventListener('pointerdown', function(){
    isSliding = true;
    pendingFilterSnapshot = { lines: JSON.parse(JSON.stringify(lines)), deleted:Array.from(deleted_idxs), selected:Array.from(selected_line_idxs) };
});
lengthFilter.addEventListener('input', function(){
    let lv = +this.value;
    lengthValueLabel.textContent = lv;
    applyFilterLive(lv);
});
function finalizeFilterCommit(){
    let startSnapshot = pendingFilterSnapshot || committedSnapshot;
    let lv = +lengthFilter.value;
    if (lv !== committedFilterValue && startSnapshot){
        let hist = {
            type: 'filter',
            dat: JSON.parse(JSON.stringify({ lv_prev: committedFilterValue, lv_new: lv })),
            lines: JSON.parse(JSON.stringify(startSnapshot.lines)),
            deleted: Array.from(startSnapshot.deleted),
            selected: Array.from(startSnapshot.selected)
        };
        undoStack.push(hist);
        if(undoStack.length>maxHistory) undoStack.shift();
        redoStack.length=0;
        refreshUndoRedoBtn();
        committedFilterValue = lv;
        committedSnapshot = { lines: JSON.parse(JSON.stringify(lines)), deleted:Array.from(deleted_idxs), selected:Array.from(selected_line_idxs) };
    }
    isSliding = false;
    pendingFilterSnapshot = null;
}
document.addEventListener('pointerup', function(){ if(isSliding) finalizeFilterCommit(); });
lengthFilter.addEventListener('change', finalizeFilterCommit);

/* 删除选中 */
function doDeleteSelected(){
    if(selected_line_idxs.size===0) return;
    pushHistory('delete',{ sel: Array.from(selected_line_idxs) });
    Array.from(selected_line_idxs).forEach(i=>deleted_idxs.add(i));
    selected_line_idxs.clear();
    redrawAll();
    document.getElementById('status').textContent = `删除选中线。`;
}
document.getElementById('deleteSelected').onclick = doDeleteSelected;

/* 下载 */
document.getElementById('downloadPLT').onclick = function(){
    let linesToExport = lines.filter((_,i)=>!deleted_idxs.has(i));
    let hpgl='IN;\n';
    for(let line of linesToExport){
        if(!line.length) continue;
        hpgl+=`PU ${line[0][0]},${line[0][1]};\n`;
        if(line.length>1) hpgl+='PD'+line.slice(1).map(([x,y])=>` ${x},${y}`).join(',')+';\n';
    }
    let blob=new Blob([hpgl],{type:'text/plain'});
    let link=document.createElement('a');
    link.href=URL.createObjectURL(blob); link.download="edited.plt"; link.click();
    document.getElementById('status').textContent = "已生成并下载新PLT文件";
};

/* 按键处理：增加 Backspace 等同 Delete */
document.addEventListener('keydown', function(e){
    if(document.activeElement && ['INPUT','TEXTAREA'].includes(document.activeElement.tagName)) return;
    if(e.key==='Delete' || e.key==='Backspace'){ doDeleteSelected(); e.preventDefault(); }
    else if(e.key==='Escape'){ selected_line_idxs.clear(); redrawAll(); }
    else if((e.ctrlKey||e.metaKey) && (e.key==='z' || e.key==='Z')){ doUndo(); e.preventDefault(); }
    else if((e.ctrlKey||e.metaKey) && (e.key==='y' || e.key==='Y')){ doRedo(); e.preventDefault(); }
});

/* 缩放/移动/全图/撤销重做按钮 */
function moveView(dx,dy){ viewX+=dx; viewY+=dy; redrawAll(); }
function zoomView(scale,centerX,centerY){
    let mx = typeof centerX==="number"?centerX:canvas.clientWidth/2;
    let my = typeof centerY==="number"?centerY:canvas.clientHeight/2;
    let old_zoom=zoom;
    zoom=Math.max(zoom*scale,0.03);
    viewX=mx-(mx-viewX)*(zoom/old_zoom);
    viewY=my-(my-viewY)*(zoom/old_zoom);
    redrawAll();
}
document.getElementById('btnZoomIn').onclick = ()=>zoomView(1.15);
document.getElementById('btnZoomOut').onclick = ()=>zoomView(0.86);
document.getElementById('btnReset').onclick = function(){ autoscaleView(); redrawAll(); };

/* 全屏切换 */
const btnFull = document.getElementById('btnFull');
btnFull.addEventListener('click', function(){
    if(!document.fullscreenElement){
        canvasBox.requestFullscreen?.();
    }else{
        document.exitFullscreen?.();
    }
});
document.addEventListener('fullscreenchange', function(){
    // 进入/退出全屏后立即并在 next frame 适配 canvas 尺寸
    resizeCanvasToFit();
    requestAnimationFrame(resizeCanvasToFit);
});

/* 撤销/重做 */
function doUndo(){
    if(undoStack.length==0) return;
    let hist = undoStack.pop();
    redoStack.push({
        type: hist.type,
        dat: JSON.parse(JSON.stringify(hist.dat)),
        lines: JSON.parse(JSON.stringify(lines)),
        deleted: Array.from(deleted_idxs),
        selected: Array.from(selected_line_idxs)
    });
    restoreHistory(hist);
    if(hist.type==='filter' && hist.dat && typeof hist.dat.lv_prev!=='undefined'){
        committedFilterValue = hist.dat.lv_prev;
        committedSnapshot = { lines: JSON.parse(JSON.stringify(hist.lines)), deleted:Array.from(hist.deleted), selected:Array.from(hist.selected) };
    }
    refreshUndoRedoBtn();
}
function doRedo(){
    if(redoStack.length==0) return;
    let hist = redoStack.pop();
    undoStack.push({
        type: hist.type,
        dat: JSON.parse(JSON.stringify(hist.dat)),
        lines: JSON.parse(JSON.stringify(lines)),
        deleted: Array.from(deleted_idxs),
        selected: Array.from(selected_line_idxs)
    });
    restoreHistory(hist);
    if(hist.type==='filter' && hist.dat && typeof hist.dat.lv_new!=='undefined'){
        committedFilterValue = hist.dat.lv_new;
        committedSnapshot = { lines: JSON.parse(JSON.stringify(hist.lines)), deleted:Array.from(hist.deleted), selected:Array.from(hist.selected) };
    }
    refreshUndoRedoBtn();
}
document.getElementById('btnUndo').onclick = doUndo;
document.getElementById('btnRedo').onclick = doRedo;

/* 辅助：窗口大小变化时调整 canvas */
window.addEventListener('resize', function(){
    resizeCanvasToFit();
});

/* 初始设置 */
function init(){
    setTimeout(resizeCanvasToFit, 40);
    refreshUndoRedoBtn();
    redrawAll();
}
init();

</script>
</body>
</html>